package com.cx.service.impl;

import com.alibaba.fastjson.JSON;
import com.cx.common.cons.CharacterCons;
import com.cx.common.util.*;
import com.cx.dal.dao.CxUserLogMapper;
import com.cx.dal.dao.CxUserLoginRecordMapper;
import com.cx.dal.dao.CxUserMapper;
import com.cx.meta.cache.CxCommonCache;
import com.cx.meta.dto.CxUser;
import com.cx.meta.dto.CxUserLog;
import com.cx.meta.dto.CxUserLoginRecord;
import com.cx.meta.entity.UserClient;
import com.cx.meta.entity.UserRegister;
import com.cx.meta.enums.CertificateType;
import com.cx.meta.enums.ErrCode;
import com.cx.meta.enums.OprResult;
import com.cx.meta.enums.UserLogType;
import com.cx.meta.exception.CxErrorCode;
import com.cx.meta.exception.CxException;
import com.cx.meta.model.po.UserQueryParams;
import com.cx.meta.model.vo.PageData;
import com.cx.meta.model.vo.UserInfo;
import com.cx.meta.model.vo.UserToken;
import com.cx.service.UserService;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.google.common.collect.Lists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.core.task.TaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;

import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Date;
import java.util.List;

/**
 * @author flysand
 * @package com.cx.service.impl
 * @date 2021-11-07 18:07
 * @des UserServiceImpl :
 */
@Service
public class UserServiceImpl extends CommonServiceImpl implements UserService {

    private final static Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);

    private final static int FIVE_MINUTE = 5 * 60;

    private final static int KEY_COUNT = 5;

    private final static List<String> IP_LIST = Lists.newArrayList("192.", "10.", "127.", "0:");

    @Resource(name = "taskExecutor")
    private TaskExecutor taskExecutor;

    @Resource
    private CxUserMapper userMapper;

    @Resource
    private CxUserLoginRecordMapper userLoginRecordMapper;

    @Resource
    private CxUserLogMapper userLogMapper;


    @Override
    public String getMobileCode(String mobile, int second, boolean verifyFreeze) {
        // 获取code  暂时用jedis  后面改为jedis集群
        Jedis jedis = JedisUtils.connRedis();

        // 判断是否需要校验当前手机号是否已冻结
        verifyMobileCount(mobile, verifyFreeze, jedis);

        //验证redis是否有手机号对应未过期的key = 业务编码+手机号+日+时间类型
        String codeKey = getCodeKey(mobile, CodeUtils.CodeKeyType.CODE);

        String code = jedis.get(codeKey);
        if (StringUtils.isEmpty(code)) {
            // 不存在 重新获取code并存入redis
            code = CodeUtils.getVerifyCode();
            second = second == 0 ? FIVE_MINUTE : second;
            jedis.set(codeKey, code, SetParams.setParams().ex(second));
        }
        JedisUtils.closeConn();
        return code;
    }

    /**
     * 是否需要校验冻结，并返回冻结次数key
     *
     * @param mobile       手机号
     * @param verifyFreeze 是否冻结
     * @param jedis        jedis
     * @return count
     */
    private String verifyMobileCount(String mobile, boolean verifyFreeze, Jedis jedis) {
        if (verifyFreeze) {
            // 校验手机号验证码错误次数  每天（24小时之内）输入错误5次  则当日（24小时内）不可再接收验证码进行验证
            String countKey = getCodeKey(mobile, CodeUtils.CodeKeyType.MOBILE_COUNT);
            String mobileCount = jedis.get(countKey);
            if (StringUtils.isNotEmpty(mobileCount) && Integer.parseInt(mobileCount) >= KEY_COUNT) {
                // 已错误5次
                throw new CxException(ErrCode.MOBILE_FREEZE);
            }
            return countKey;
        }
        return null;
    }

    @Override
    public boolean verifyCode(String mobile, String code, boolean verifyFreeze) {

        Jedis jedis = JedisUtils.connRedis();
        // 校验是否冻结
        String countKey = verifyMobileCount(mobile, verifyFreeze, jedis);


        String codeKey = getCodeKey(mobile, CodeUtils.CodeKeyType.CODE);

        String codeVal = jedis.get(codeKey);
        if (StringUtils.isEmpty(codeVal)) {
            // key已过期
            if (null != countKey) {
                // 次数+1
                jedis.incr(countKey);
            }
            throw new CxException(ErrCode.CODE_EXPIRED);
        }
        if (!codeVal.equals(code)) {
            // 验证码不正确
            if (null != countKey) {
                // 次数+1
                jedis.incr(countKey);
            }
            throw new CxException(ErrCode.CODE_ERROR);
        }
        // 验证成功，删除key
        jedis.del(codeKey);
        jedis.del(countKey);
        JedisUtils.closeConn();
        return true;
    }

    /**
     * 根据keyType获取不同类型的key
     *
     * @param mobile  手机号
     * @param keyType key类型
     * @return key
     */
    private String getCodeKey(String mobile, CodeUtils.CodeKeyType keyType) {
        switch (keyType) {
            case CODE:
                return CodeUtils.CodeType.USER_VERIFY.getServiceCode() + CharacterCons.UNDER_LINE + mobile +
                        DateUtils.getDateString(DateUtils.DateType.DATE_WITHOUT_SPLIT) + CharacterCons.UNDER_LINE + "5";
            case MOBILE_COUNT:
                return CodeUtils.CodeType.USER_VERIFY.getServiceCode() + CharacterCons.UNDER_LINE + mobile +
                        DateUtils.getDateString(DateUtils.DateType.DATE_WITHOUT_SPLIT) + CharacterCons.UNDER_LINE + "count";
            default:
                //
        }
        return null;
    }

    @Override
    public CxUser registerUser(UserRegister user) {
        // 生成密码
        CxUser cxUser = new CxUser();
        BeanUtils.copyProperties(user, cxUser);
        if (StringUtils.isBlank(cxUser.getName())) {
            cxUser.setName(cxUser.getUsername());
        }
        String userId = RandomUtils.getUuid();
        cxUser.setId(userId);
        // 获取加密后的密码
        cxUser.setPassword(generatePwd(userId, user.getPassword()));

        userMapper.insertSelective(cxUser);
        return cxUser;
    }


    private String getMd5Password(String password, String salt, Md5Utils.SaltMode saltMode) {
        return Md5Utils.md5Salt(password, salt, saltMode == null ? Md5Utils.SaltMode.SUFFIX_REPEAT : saltMode);
    }

    /**
     * 生成用户的加盐密码
     *
     * @param userId   用户id
     * @param password 用户密码
     * @return 生成的加盐密码
     */
    private String generatePwd(String userId, String password) {
        String salt = Md5Utils.getSalt(userId, Md5Utils.SaltSource.UUID);
        return getMd5Password(password, salt, Md5Utils.SaltMode.PRE_REPEAT);
    }

    @Override
    public PageData<UserInfo> getUserByQueryParams(UserQueryParams queryParams) {
        // 分页查询
        int page = queryParams.getPage() == 0 ? 1 : queryParams.getPage();
        int rows = queryParams.getRows() == 0 ? 10 : queryParams.getRows();
        String orderBy = "";
        String sort = queryParams.getSort();
        if (StringUtils.isNotBlank(sort)) {
            sort = CamelCaseUtils.humpToLine(sort);
            orderBy = sort + " desc";
        }
        if (StringUtils.isNotBlank(queryParams.getOrder())) {
            orderBy = orderBy.replace("desc", queryParams.getOrder());
        }

        if (StringUtils.isBlank(orderBy)) {
            PageHelper.startPage(page, rows);
        } else {
            PageHelper.startPage(page, rows, orderBy);
        }
        PageHelper.startPage(page, rows);
        Page<UserInfo> users = userMapper.getByParams(queryParams);

        PageData<UserInfo> pageData = new PageData<>();
        pageData.setTotal((int) users.getTotal());
        pageData.setRows(users);
        return pageData;
    }

    @Override
    public CxUser getUserByMobile(String mobile) {
        return userMapper.selectByMobile(mobile);
    }

    @Override
    public String mobileLogin(String mobile, String code, UserClient userClient) {
        // 校验验证码
        if (verifyCode(mobile, code, Boolean.TRUE)) {
            // 验证成功后，若用户不存在 则注册用户
            CxUser user = getUserByMobile(mobile);
            String cause = "手机号登录";
            if (user == null) {
                // 注册
                UserRegister register = new UserRegister();
                register.setMobileNumber(mobile);
                register.setUsername(mobile);
                register.setName(mobile);
                user = registerUser(register);
                cause = "手机号注册";
            }

            // 记录登录成功
            recordUserLogin(user.getId(), CertificateType.MOBILE, userClient.getIp(), userClient.getAddress(), userClient.getBrowser(), OprResult.SUCCESS, cause);

            // 生成token  后续移动端 考虑增加设备id
            return generateToken(user, userClient.getIp());
        }
        // 验证码错误
        throw new CxException(CxErrorCode.CODE_ERROR);
    }

    /**
     * 生成token
     *
     * @param user 用户
     * @param ip   ip
     * @return token
     */

    private String generateToken(CxUser user, String ip) {
        // TODO 暂时根据user_id生成token ip+user生成value
        String token = getLoginToken(user.getId());
        // 2021-11-21 01:44 ip可能要解析，所以修改其他可解密的算法
        String base64Ip = Base64.getEncoder().encodeToString(ip.getBytes(StandardCharsets.UTF_8));
        // 加到后面
        String val = Md5Utils.md5(ip) + CharacterCons.UNDER_LINE + JSON.toJSONString(user) + CharacterCons.UNDER_LINE + base64Ip;
        // 写入redis
        try {
            JedisUtils.connRedis().set(token, val, SetParams.setParams().ex(24 * 60 * 60));
        } catch (Exception e) {
            // redis 写入失败  -- 记录日志 可能redis挂了
            logger.error("redis写入失败，请检查redis服务器");
            logger.error(String.format("用户%s在[ip:%s]登录，写入token:%s时，异常，请检查redis服务器", user.getId(), ip, token));
            throw new CxException(CxErrorCode.REDIS_SERVER_EXCEPTION);
        } finally {
            JedisUtils.closeConn();
        }
        return token;
    }

    @Override
    public String userLogin(String loginName, String password, UserClient userClient) {
        // 根据登录名查询用户
        CxUser user = userMapper.selectByLoginName(loginName);
        if (user == null) {
            // 用户不存在
            throw new CxException(CxErrorCode.LOGIN_NAME_ERROR);
        }
        // 校验密码
        if (!user.getPassword().equals(generatePwd(user.getId(), password))) {
            // 记录登录失败
            recordUserLogin(user.getId(), CertificateType.USERNAME, userClient.getIp(), userClient.getAddress(), userClient.getBrowser(), OprResult.FAILED, CxErrorCode.PASSWORD_ERROR.getErrMsg());
            throw new CxException(CxErrorCode.PASSWORD_ERROR);
        }
        // 记录登录成功
        recordUserLogin(user.getId(), CertificateType.USERNAME, userClient.getIp(), userClient.getAddress(), userClient.getBrowser(), OprResult.SUCCESS, null);

        // 生成token
        return generateToken(user, userClient.getIp());
    }

    /**
     * 新建线程写入登录流水
     *
     * @param userId      用户id
     * @param ip          ip
     * @param loginAddr   地址
     * @param clientInfo  客户端信息  --  浏览器信息 或者 移动端
     * @param loginResult 登录结果
     * @param cause       登录失败原因
     */
    private void recordUserLogin(String userId, CertificateType loginType, String ip, String loginAddr, String clientInfo, OprResult loginResult, String cause) {
        // 新建线程执行
        taskExecutor.execute(() -> {
            CxUserLoginRecord record = new CxUserLoginRecord(RandomUtils.getUuid(), userId, loginType, new Date(), ip, loginAddr, clientInfo, loginResult, cause, new Date());
            userLoginRecordMapper.insertSelective(record);
        });


    }

    /**
     * 获取登录token
     *
     * @param userId 用户id
     * @return 认证token
     */
    private String getLoginToken(String userId) {
        return getMd5Password(userId, Md5Utils.getSalt(userId, Md5Utils.SaltSource.UUID), Md5Utils.SaltMode.SUFFIX);
    }

    @Override
    public UserToken getUserToken(String token) {
        // 毫秒
        Long ptl = JedisUtils.connRedis().pttl(token);
        // 从redis拿吧  --  可以增加服务器自己过几分钟查询   就有点延迟
        // 直接从数据库取  ---  会增加数据库查询
        String val = JedisUtils.connRedis().get(token);
        String[] valArr = val.split(CharacterCons.UNDER_LINE);
        CxUser user = JSON.parseObject(valArr[1], CxUser.class);
        UserToken userToken = new UserToken(token, ptl);
        userToken.setSignature(user.getSignature());
        userToken.setName(StringUtils.isBlank(user.getName()) ? user.getUsername() : user.getName());
        userToken.setState(user.getState());
        userToken.setHead(user.getHead());

        String ip = new String(Base64.getDecoder().decode(valArr[2]));
        if (StringUtils.isNotBlank(ip) & !CollectionUtils.startWith(ip, IP_LIST)) {
            userToken.setIp(ip);
        }
        return userToken;
    }

    @Transactional(rollbackFor = CxException.class)
    @Override
    public String modifySignature(String token, String signature) {
        // 先从本地缓存拿  本地缓存没有再去redis拿
        String val = CxCommonCache.TOKEN_MAP.get(token);
        if (StringUtils.isBlank(val)) {
            val = JedisUtils.connRedis().get(token);
        }
        CxUser redisUser = JSON.parseObject(val.split(CharacterCons.UNDER_LINE)[1], CxUser.class);

        String userId = redisUser.getId();
        // 获取用户信息
        CxUser cxUser = userMapper.selectByPrimaryKey(userId);
        if (cxUser.getSignature().equals(signature)) {
            // 如果当前要修改的签名和之前一样，就不修改
            logger.debug("签名跟之前一样，不需要修改");
            return cxUser.getSignature();
        }
        CxUser user = new CxUser();
        user.setId(userId);
        user.setSignature(signature);
        int count = userMapper.updateByPrimaryKeySelective(user);
        if (count > 0) {
            // 异步插入
            taskExecutor.execute(() -> {

                String logInfo = "用户修改了个性签名：" + (StringUtils.isBlank(cxUser.getSignature()) ? "" : cxUser.getSignature())
                        + " =>> " + signature;
                CxUserLog log = new CxUserLog(RandomUtils.getUuid(), userId, null, UserLogType.SIGNATURE, logInfo, null);
                // 插入用户信息变更记录
                userLogMapper.insertSelective(log);
            });
        }

        return signature;
    }
}
